﻿local new, del
do
	local cache = setmetatable({},{__mode='k'})
	function new()
		local t = next(cache)
		if t then
			cache[t] = nil
			return t
		else
			return {}
		end
	end
	function del(t)
		for k, _ in pairs(t) do
			t[k] = nil
		end
		cache[t] = true
		return nil
	end
end

DarkCandy = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0", "AceDB-2.0", "AceConsole-2.0", "AceDebug-2.0", "CandyBar-2.0")
local DewDrop = AceLibrary("Dewdrop-2.0")
local Parser = ParserLib:GetInstance("1.1")
local PaintChips = AceLibrary("PaintChips-2.0")
local Surface = AceLibrary("Surface-1.0")

local path = "Interface\\AddOns\\DarkCandy\\Textures\\"
Surface:Register("BantoBar", path.."banto")
Surface:Register("Smooth", path.."smooth")
Surface:Register("Perl", path.."perl")
Surface:Register("Glaze", path.."glaze")
Surface:Register("Cilo", path.."cilo")
Surface:Register("Charcoal", path.."Charcoal")
Surface:Register("Steel", path.."Steel")

DarkCandy.SPELL = 1
DarkCandy.EVENT = 2

local timers = {}
local spellBars = {}
local activeSpells = {}

DarkCandy.defaults = {
	scale = 1,
	growup = false,
	reverse = false,
	fadeonkill = true,
	fadeonfade = true,
	pos = {},
	texture = "default",
	ghost = 0.5,
	width = 255,
	height = 15,
	bgcolor = "00FF00",
	barcolor = "FF0000",
	bgalpha = 1,
}

DarkCandy.options = {
	type = "group",
	handler = DarkCandy,
	args = {
		anchor = {
			name = "Anchor", 
			desc = "Shows the dragable anchor.", 
			type = "execute", 
            func = function()
                if DarkCandy.anchor:IsVisible() then
                    DarkCandy.anchor:Hide()
                else
                    DarkCandy.anchor:Show()
                end
            end
		},
		bar = {
			name = "Bar",
			desc="CandyBar options", 
			type = "group",
			args = {
				texture = {
					name = "Bar Texture", 
					desc = "Changes the texture of the timer bars.", 
					type = "text",
					get = function () return DarkCandy.db.profile.texture end,
					set = function (v) DarkCandy.db.profile.texture = v end,
					validate = Surface:List(),
				},
				color = {
					name = "Bar Color", 
					desc = "Set the default bar color.",
                    type = "color",
                    get = function()
                        local _, r, g, b = PaintChips:GetRGBPercent(DarkCandy.db.profile.barcolor)
                        return r, g, b 
                    end,
                    set = function(r, g, b, a) 
                        local hex = DarkCandy:RGBToHex(r, g, b)
                        PaintChips:RegisterHex(hex)
                        DarkCandy.db.profile.barcolor = hex 
                    end,
				},
				bgcolor = {
					name = "Background Color", 
					desc = "Set the bar background color.", 
                    type = "color",
                    get = function()
                        local _, r, g, b = PaintChips:GetRGBPercent(DarkCandy.db.profile.bgcolor)
                        return r, g, b 
                    end,
                    set = function(r, g, b, a) 
                        local hex = DarkCandy:RGBToHex(r, g, b)
                        PaintChips:RegisterHex(hex)
                        DarkCandy.db.profile.bgcolor = hex 
                    end,
				},
				bgalpha = {
					name = "Background Alpha",
					desc = "Alpha value of the bar background.",
					type = "range",
					get = function () return DarkCandy.db.profile.bgalpha end,
					set = function (v) DarkCandy.db.profile.bgalpha = v end,
					min = 0.0, max = 1.0,
				},
				scale = {
					name = "Bar Scale", 
					desc = "Set the bar scale.", 
					type = "range",
					get = function () return DarkCandy.db.profile.scale end,
					set = function (v) DarkCandy.db.profile.scale = v end,
					min = 0.5, max = 1.5,
				},
				height = {
					name = "Bar Height", 
					desc = "Set the bar height.", 
					type = "range",
					get = function () return DarkCandy.db.profile.height end,
					set = function (v) DarkCandy.db.profile.height = v end,
					min = 8, max = 30,
					step = 1,
				},
				width = {
					name = "Bar Width", 
					desc = "Set the bar width.", 
					type = "range",
					get = function () return DarkCandy.db.profile.width end,
					set = function (v) DarkCandy.db.profile.width = v end,
					min = 50, max = 300,
					step = 1,
				},
				growth = {
					name = "Bar Growth", 
					desc = "Toggles bar growing up or downwards.", 
					type = "toggle",
					get = function () return DarkCandy.db.profile.growup end,
					set = function (v) DarkCandy.db.profile.growup = v
					DarkCandy:SetCandyBarGroupGrowth("DarkCandy", DarkCandy.db.profile.growup) end,
				},
				reverse = {
					name = "Reverse", 
					desc = "Toggles if bars are reversed (fill up instead of emptying).", 
					type = "toggle",
					get = function() return DarkCandy.db.profile.reverse end,
					set = function(f) DarkCandy.db.profile.reverse = f end,
				},
			},
		},
		kill = {
			name = "Kill", 
			desc = "Toggles whether bars disappear when killing things.", 
			type = "toggle",
			get = function() return DarkCandy.db.profile.fadeonkill end,
			set = function(v)
                DarkCandy.db.profile.fadeonkill = v 
                if v then
                    Parser:RegisterEvent("DarkCandy", "CHAT_MSG_COMBAT_HOSTILE_DEATH")
                else
                    Parser:UnregisterEvent("DarkCandy", "CHAT_MSG_COMBAT_HOSTILE_DEATH")
                end
            end
        },
		fade = {
			name = "Fade", 
			desc = "Toggles whether bars disappear when spells fade.", 
			type = "toggle",
			get = function() return DarkCandy.db.profile.fadeonfade end,
			set = function(v)
                DarkCandy.db.profile.fadeonfade = v
                if v then
                    Parser:RegisterEvent("DarkCandy", "CHAT_MSG_SPELL_AURA_GONE_OTHER")
                else
                    Parser:UnregisterEvent("DarkCandy", "CHAT_MSG_SPELL_AURA_GONE_OTHER")
                end
            end
        },
	},
}

DarkCandy:RegisterDB("DarkCandyDB")
DarkCandy:RegisterDefaults('profile', DarkCandy.defaults)
DarkCandy:RegisterChatCommand({'/darkcandy'}, DarkCandy.options)

function DarkCandy:OnInitialize()
end

function DarkCandy:Setup()
	self:AddTimer(self.SPELL, "Banish",	"Spell_Shadow_Cripple", 30, { gr=1, rt={20} })
	self:AddTimer(self.SPELL, "Corruption", "Spell_Shadow_AbominationExplosion", 18, { gr=2, rt={12,15} })
	self:AddTimer(self.SPELL, "Curse of Agony", "Spell_Shadow_CurseOfSargeras",	24, { gr=3 })
	self:AddTimer(self.SPELL, "Curse of Doom", "Spell_Shadow_AuraOfDarkness", 60, { gr=3 })
	self:AddTimer(self.SPELL, "Curse of Exhaustion", "Spell_Shadow_GrimWard", 12, { gr=3 })
	self:AddTimer(self.SPELL, "Curse of Recklessness", "Spell_Shadow_UnholyStrength", 120, { gr=3 })
	self:AddTimer(self.SPELL, "Curse of Shadow", "Spell_Shadow_CurseOfAchimonde", 300, { gr=3 })
	self:AddTimer(self.SPELL, "Curse of Tongues", "Spell_Shadow_CurseOfTounges", 30, { gr=3 })
	self:AddTimer(self.SPELL, "Curse of Weakness", "Spell_Shadow_CurseOfMannoroth", 120, { gr=3 })
	self:AddTimer(self.SPELL, "Curse of the Elements", "Spell_Shadow_ChillTouch", 300, { gr=3 })
	self:AddTimer(self.SPELL, "Death Coil", "Spell_Shadow_DeathCoil", 3, { gr=4 })
	self:AddTimer(self.SPELL, "Fear", "Spell_Shadow_Possession", 20, { gr=5, rt={10,15} })
	self:AddTimer(self.SPELL, "Howl of Terror", "Spell_Shadow_DeathScream", 8, { gr=6, rt={6} })
	self:AddTimer(self.SPELL, "Immolate", "Spell_Fire_Immolation", 15, { gr=7 })
	self:AddTimer(self.SPELL, "Shadowfury",	"Spell_Shadow_Shadowfury", 2, { gr=8 })
	self:AddTimer(self.SPELL, "Siphon Life", "Spell_Shadow_Requiem", 30, { gr=9 })
	self:AddTimer(self.SPELL, "Unstable Affliction",  "Spell_Shadow_UnstableAffliction_3", 18, { gr=10 })
	self:AddTimer(self.SPELL, "Enslave Demon", "Spell_Shadow_EnslaveDemon", 300, { gr=11 })
	self:AddTimer(self.EVENT, "Seduction", "Spell_Shadow_MindSteal", 15, { gr=12 })
    
	self:AddTimer(self.SPELL, "Conflagrate", "", 0,	{ r="Immolate" })	
end

function DarkCandy:OnEnable()
	DewDrop:InjectAceOptionsTable(self, self.options)
	
	self:RegisterChatCommand('/darkcandy', {
		type = 'execute',
		func = function()
			DewDrop:Open("DarkCandy")
		end
	})
	
	DewDrop:Register("DarkCandy",
		'children', DarkCandy.options,
		'cursorX', true,
		'cursorY', true
	)
	
	-- create the anchor frame
	self:CreateAnchor()
	
	self:RegisterCandyBarGroup("DarkCandy")
	self:SetCandyBarGroupPoint("DarkCandy", "TOP", self.anchor, "BOTTOM", 0, 0)
	self:SetCandyBarGroupGrowth("DarkCandy", self.db.profile.growup)
		
	self:Setup()

	-- On-Kill handling
    if self.db.profile.fadeonkill then
        Parser:RegisterEvent("DarkCandy", "CHAT_MSG_COMBAT_HOSTILE_DEATH",	function(event, info) self:COMBAT_DEATH(event, info) end)
    end
    
	-- Spell fade handling
    if self.db.profile.fadeonfade then
        Parser:RegisterEvent("DarkCandy", "CHAT_MSG_SPELL_AURA_GONE_OTHER", function(event, info) self:SPELL_FADE(event, info) end)
    end
    
	-- Buff/Debuff gain handling
	Parser:RegisterEvent("DarkCandy", "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)
	Parser:RegisterEvent("DarkCandy", "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", function (event, info) self:SPELL_PERIODIC(event, info) end)

	-- Enslave
	Parser:RegisterEvent("DarkCandy", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", function(event, info) self:SPELL_PERIODIC(event, info) end)

	-- Failure
	Parser:RegisterEvent("DarkCandy", "CHAT_MSG_SPELL_FAILED_LOCALPLAYER", function(event, info) self:SPELL_FAILED(event, info) end)
	
    -- Spellcasting
	self:RegisterEvent("UNIT_SPELLCAST_SENT")
	self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
	Parser:RegisterEvent("DarkCandy", "CHAT_MSG_SPELL_SELF_DAMAGE", function(event, info) self:SELF_DAMAGE(event, info) end)
end

function DarkCandy:OnDisable()
	for _,v in ipairs(spellBars) do
		for _, va in ipairs(v) do
			if v.id then
				self:StopCandyBar(v.id)
				v = del(v)
			end
		end
		va = del(va)
	end
	
	if timers then timers = nil end
	if activeSpells then activeSpells = nil end

	Parser:UnregisterAllEvents("DarkCandy")
	self:UnregisterAllEvents()
end

function DarkCandy:RGBToHex(r, g, b)
	return format("%02x%02x%02x", r*255, g*255, b*255)
end

function DarkCandy:AddTimer(kind, name, icon, duration, extra)
	if not timers[kind]	   then timers[kind] = {}		 end
	if not timers[kind][name] then timers[kind][name] = {} end
	if not extra then extra = {} end
	timers[kind][name] = { i=icon, d=duration, gr=(extra.gr or nil), rt=(extra.rt or nil),r=(extra.r or nil) }
end

function DarkCandy:StartTimer(timer, name, target, rank, durmod)
	if target == "" or target == nil then target = "none" end	
	if not spellBars[target] then spellBars[target] = new() end

	-- Remove group spells, there can be only one
	local group = timer.gr
	if group then
		for _, v in ipairs(spellBars[target]) do
			if v.group == group then
                self:SetCandyBarFade(v.id, 0, false)
				self:StopCandyBar(v.id)
				v = del(v)
				break
			end
		end
	end
	
	-- Check if spell is already active
--	for _, v in ipairs(spellBars[target]) do
--		if v.name == name then
--			self:SetCandyBarFade(v.id, 0, false)
--			self:StopCandyBar(v.id)
--			v = del(v)
--			break
--		end
--	end

    -- Create us a new bar and insert it
	local id = name .. "-" .. target
	local bar = new()
	bar.id, bar.name, bar.group = id, name, timer.gr
	table.insert(spellBars[target], bar)
    self:Debug(#spellBars[target], spellBars[target])

    rank = rank or 0
	local duration = ((timer.rt and timer.rt[rank]) or timer.d) + (durmod or 0) 
	local text = (target == "none" and name) or target .. " - " .. name
    local icon = "Interface\\Icons\\" .. timer.i
    
    local db = self.db.profile
	self:RegisterCandyBar(id, duration, text, icon, db.barcolor)
	self:RegisterCandyBarWithGroup(id, "DarkCandy")
	self:SetCandyBarScale(id, db.scale)
	self:SetCandyBarWidth(id, db.width)
	self:SetCandyBarHeight(id, db.height)
	self:SetCandyBarTexture(id, Surface:Fetch(db.texture))
	self:SetCandyBarBackgroundColor(id, db.bgcolor, db.bgalpha)
	self:SetCandyBarFade(id, db.ghost, true)
	self:SetCandyBarCompletion(id, self.StopBar, self, id, target)
	self:SetCandyBarReversed(id, db.reverse)
	self:SetCandyBarOnClick(id, function(id, button) self:OnClick(id, button) end)
	self:StartCandyBar(id, true)
end

--<< ====================================================================== >>--
-- Bar Processing															  --
--<< ====================================================================== >>--
function DarkCandy:IsBanished(target)
	local bars = spellBars[target]
	if bars then
		for _, v in ipairs(bars) do
			if v.name == "Banish" then
				return true
			end
		end
	end
	return false
end

function DarkCandy:KillBar(id, target)
	local bars = spellBars[target]
	if bars then
		for _, v in ipairs(bars) do
			if v.id == id then
				self:StopCandyBar(v.id)
				v = del(v)
				break
			end
		end
	end
end

function DarkCandy:KillBars(target)
	if target and UnitExists("target") and UnitName("target") == target and not UnitIsDeadOrGhost("target") then return end
	local bars = spellBars[target]
	if bars then
		for _, v in ipairs(bars) do
			if v.id then
				self:StopCandyBar(v.id)
				v = del(v)
			end
		end
		bars = del(bars)
		spellBars[target] = del(spellBars[target])
	end
end

function DarkCandy:StopBar(id, target)
	local bars = spellBars[target]
	if bars then
		for _, v in ipairs(bars) do
			if v.id == id then
				v = del(v)
				break
			end
		end
	end
end

function DarkCandy:ReportStatus(id, target)
	local bars = spellBars[target]
	if bars then
		for _, v in ipairs(bars) do
			if v.id == id then
                local reg, t, e, r = self:CandyBarStatus(id)
                self:Debug(v.name .. " fades in " .. t - e .. "s") 
				break
			end
		end
	end
end

function DarkCandy:OnClick(id, button, target)
    local target = string.match(id, "-(.+)")
    self:Debug(target)
	if button == "RightButton" then
        self:KillBar(id, target)
	elseif button == "LeftButton" then
        self:ReportStatus(id, target)
    end
end

function DarkCandy:CreateAnchor()
	self.anchor = CreateFrame("Button", nil, UIParent)
    
    local db = self.db.profile
    
	self.anchor:SetWidth(db.width)
	self.anchor:SetHeight(db.height)

	if db.pos.x and db.pos.y then
		self.anchor:ClearAllPoints()
		self.anchor:SetPoint("TOPLEFT", UIParent, "TOPLEFT", db.pos.x, db.pos.y)
	else
		self.anchor:SetPoint("CENTER", UIParent, "CENTER", 0, 50)
	end

	self.anchor:SetScript("OnDragStart", function() this:StartMoving() end )
	self.anchor:SetScript("OnDragStop", function()
		this:StopMovingOrSizing()
		local _, _, _, x, y = this:GetPoint()
		DarkCandy.db.profile.pos.x = math.floor(x)
		DarkCandy.db.profile.pos.y = math.floor(y)
	end)

	self.anchor:SetBackdrop({
		bgFile = "Interface/Tooltips/UI-Tooltip-Background",
		edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
		tile = false, tileSize = 16, edgeSize = 16,
		insets = { left = 5, right =5, top = 5, bottom = 5 
	}})
	self.anchor:SetBackdropColor(0,1,0,.6)
	self.anchor:SetMovable(true)
	self.anchor:RegisterForDrag("LeftButton")

	self.anchor.Text = self.anchor:CreateFontString(nil, "OVERLAY")
	self.anchor.Text:SetFontObject(GameFontNormalSmall)
	self.anchor.Text:ClearAllPoints()
	self.anchor.Text:SetTextColor(1, 1, 1, 1)
	self.anchor.Text:SetWidth(200)
	self.anchor.Text:SetHeight(25)
	self.anchor.Text:SetPoint("TOPLEFT", f, "TOPLEFT")
	self.anchor.Text:SetJustifyH("CENTER")
	self.anchor.Text:SetJustifyV("MIDDLE")
	self.anchor.Text:SetText("DarkCandy")

	self.anchor:Hide()
end

function DarkCandy:COMBAT_DEATH(event, info)
	if not self.db.profile.fadeonkill then return end
	if info.victim == ParserLib_SELF then return end
	
	if info.victim and not self:IsBanished(info.victim) then
		return self:KillBars(info.victim)
	end
end

function DarkCandy:SPELL_FADE(event, info)
	if not self.db.profile.fadeonfade then return end
	
	if info.skill == "Banish" then return end
	if info.type == "fade" then
		self:KillBar(info.skill .. "-" .. info.victim, info.victim)
	end
end

function DarkCandy:SPELL_PERIODIC(event, info)
	if info.type ~= "debuff" or info.victim == ParserLib_SELF then return end
		
	-- Special case for seduction
	if info.skill == "Seduction" and not UnitExists("pettarget") then return end
	
	local timer = timers[self.EVENT][info.skill]
	if timer then
		self:StartTimer(timer, info.skill, info.victim)
	end
end

function DarkCandy:GetRankNumber(rank)
    local r = string.match(rank, "%d+")
    return tonumber(r)
end

function DarkCandy:UNIT_SPELLCAST_SENT(unit, spell, rank, target)
	local timer = timers[DarkCandy.SPELL][spell]
	if timer then
        local active = new()
		active.timer, active.spell, active.target, active.rank = timer, spell, target, self:GetRankNumber(rank)
        activeSpells[spell] = active
	end
end

function DarkCandy:UNIT_SPELLCAST_SUCCEEDED(unit, spell, rank)
	local active = activeSpells[spell]
	if active then
		local rank = self:GetRankNumber(rank)	
		if active.spell == spell and active.rank == rank then
            if active.timer.r then
                self:Debug(spell)
                self:KillBar(active.timer.r .. "-" .. active.target, active.target)
			elseif active.target == "none" then
				self:StartTimer(active.timer, active.spell, active.target, active.rank)
                activeSpells[spell] = nil
				active = del(active)
			else
				self:ScheduleEvent(self.CompleteCast, 0.3, self, active.spell)
			end
		end
	end
end

function DarkCandy:CompleteCast(spell)
	local active = activeSpells[spell]
	if active then
		self:StartTimer(active.timer, active.spell, active.target, active.rank, -0.25)
        activeSpells[spell] = nil
		active = del(active)
	end
end

function DarkCandy:SPELL_FAILED(event, info)
	local active = activeSpells[info.skill]
	if active then
        activeSpells[info.skill] = nil
		active = del(active)
	end
end

function DarkCandy:SELF_DAMAGE(event, info)
	local active = activeSpells[info.skill]
	if active then
		if info.type == "miss" and info.victim == active.target then
            activeSpells[info.skill] = nil
			active = del(active)
		end
	end
end